Disclaimer

This is a project I have worked on for fun, more or less in my spare time. It is not intended for, and probably shouldn’t be used for, any kind of commercial purposes. The data here comes from the very detailed https://www.bicyclerollingresistance.com/. Much credit to Jarno Bierman for all of the data

Problem Statement

Cyclists for years have wondered which is the best tire on the market for their needs. I myself have wondered this as well. Bicycle tires are primarily defined by their speed, measured by rolling resistance (lower is faster) and puncture protection (higher is better). Objective results of testing for tires is often hard to come by, until Jarno Bierman created his bicyclerollingresistance.com. Jarno uses a rolling drum to test tires on their resistance and also a test to determine puncture resistance. I make no comments on the quality of the test and whether or not it is really the best in the world, however other tests, for example from the German magazine RoadBike show directionally similar results, so I will consider these good.

Process

First, I import the data from bicyclerollingresistance.com. Lacking better skills, I copy into a csv file and import into R

Secondly, I process the data to make it into a format more suitable for analysis.

Thirdly, I present some visualizations.

Fourth, we enter our weighting criteria for tire resistance, puncture protection and weight. We do this on a scale from 1-100, with a total of only 100 for all 3 criteria.

Fifth, I normalize the test scores and multiply by the criteria.

Sixth, I score and rank the tires by the total score

Finally, I present a visualization of the results.

## -- Attaching packages ----------------------------------------------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.2.1     v purrr   0.3.3
## v tibble  2.1.3     v dplyr   0.8.3
## v tidyr   1.0.0     v stringr 1.4.0
## v readr   1.3.1     v forcats 0.4.0
## -- Conflicts -------------------------------------------------------------------------------- tidyverse_conflicts() --
## x dplyr::between()   masks data.table::between()
## x dplyr::filter()    masks stats::filter()
## x dplyr::first()     masks data.table::first()
## x dplyr::lag()       masks stats::lag()
## x dplyr::last()      masks data.table::last()
## x purrr::transpose() masks data.table::transpose()
## 
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## The following object is masked from 'package:stats':
## 
##     filter
## The following object is masked from 'package:graphics':
## 
##     layout

#Step 1, import the data

##         Brand                       Model Year TireType PriceRange   Width
## 1     Pirelli          Cinturato Velo TLR 2018       TL       High 26 / 28
## 2 Continental       Competition (tubular) 2016       TU      High+ 25 / 25
## 3    Veloflex                       Corsa 2016       TT       High 25 / 25
## 4    Vittoria Corsa Control G+ 1.0 (open) 2018       TT       High 25 / 27
## 5    Vittoria Corsa Control G+ 2.0 (open) 2019       TT       High 25 / 27
## 6    Vittoria       Corsa Elite (tubular) 2017       TU       High 25 / 25
##      Weight RR120 RR100 RR80 RR60 PunctureTest  Thickness
## 1 290 / 306  15.6  16.6 18.7 22.3       20 / 7 3.7 / 0.95
## 2 280 / 285  14.2  15.3 16.5 18.7       11 / 4 2.5 / 0.55
## 3 205 / 200  13.4  14.0 15.8 18.2       12 / 4 2.1 / 0.75
## 4 265 / 260  15.6  16.7 18.1 21.1       12 / 5 2.6 / 0.85
## 5 265 / 262  14.1  15.4 17.0 20.1       12 / 5 2.7 / 0.90
## 6 290 / 298  12.9  13.6 15.4 18.1       12 / 3 2.4 / 0.60
##     Brand              Model                Year        TireType        
##  Length:75          Length:75          Min.   :2014   Length:75         
##  Class :character   Class :character   1st Qu.:2016   Class :character  
##  Mode  :character   Mode  :character   Median :2017   Mode  :character  
##                                        Mean   :2017                     
##                                        3rd Qu.:2018                     
##                                        Max.   :2020                     
##   PriceRange           Width              Weight              RR120      
##  Length:75          Length:75          Length:75          Min.   : 7.00  
##  Class :character   Class :character   Class :character   1st Qu.:11.70  
##  Mode  :character   Mode  :character   Mode  :character   Median :13.40  
##                                                           Mean   :13.49  
##                                                           3rd Qu.:15.40  
##                                                           Max.   :19.50  
##      RR100            RR80            RR60       PunctureTest      
##  Min.   : 7.50   Min.   : 8.30   Min.   : 9.30   Length:75         
##  1st Qu.:12.50   1st Qu.:13.85   1st Qu.:16.35   Class :character  
##  Median :14.20   Median :16.00   Median :18.70   Mode  :character  
##  Mean   :14.35   Mean   :16.00   Mean   :18.80                     
##  3rd Qu.:16.60   3rd Qu.:18.55   3rd Qu.:21.80                     
##  Max.   :20.50   Max.   :23.10   Max.   :27.40                     
##   Thickness        
##  Length:75         
##  Class :character  
##  Mode  :character  
##                    
##                    
## 

Tidying the data

Some of the data is not in a correct type for analysis. Price Range, Tire Type and Brand are all character and before we go further, I will convert these to factor, as each is a discrete group.

Further tidying

Some of the fields we can see are entered as “number / number”. This makes any kind of analysis impossible so I will separate these fields with ‘separate()’ from dplyr.

tires.separated <- tires %>% 
  separate(PunctureTest, into = c('Punct.T','Punct.Side')) %>% 
  separate(Thickness, into = c('Thick.Tread','Thick.Side'), convert = F) %>% 
  separate(Width, into = c('Width.S', 'Width.M')) %>% 
  separate(Weight, into = c('Weight.S','Weight.M'))
## Warning: Expected 2 pieces. Additional pieces discarded in 72 rows [1, 2, 3, 4,
## 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...].

These new fields need to be as number and now I convert those

tires.separated$Weight.M <- as.numeric(tires.separated$Weight.M, na.rm = TRUE)
tires.separated$Punct.T <- as.numeric(tires.separated$Punct.T)
tires.separated$Thick.Tread <- as.numeric(tires.separated$Thick.Tread, na.rm = TRUE)
tires.separated$Thick.Side <- as.numeric(tires.separated$Thick.Side, na.rm = TRUE)

A quick check of the data:

summary(tires.separated)
##          Brand       Model                Year      TireType  PriceRange
##  Vittoria   :16   Length:75          Min.   :2014   TL:15    High  :50  
##  Continental:15   Class :character   1st Qu.:2016   TT:55    Medium: 0  
##  Schwalbe   :14   Mode  :character   Median :2017   TU: 5    Low   : 5  
##  Michelin   : 7                      Mean   :2017            NA's  :20  
##  Pirelli    : 4                      3rd Qu.:2018                       
##  Hutchinson : 3                      Max.   :2020                       
##  (Other)    :16                                                         
##    Width.S            Width.M            Weight.S            Weight.M    
##  Length:75          Length:75          Length:75          Min.   :163.0  
##  Class :character   Class :character   Class :character   1st Qu.:220.5  
##  Mode  :character   Mode  :character   Mode  :character   Median :245.0  
##                                                           Mean   :250.3  
##                                                           3rd Qu.:267.0  
##                                                           Max.   :399.0  
##                                                                          
##      RR120           RR100            RR80            RR60      
##  Min.   : 7.00   Min.   : 7.50   Min.   : 8.30   Min.   : 9.30  
##  1st Qu.:11.70   1st Qu.:12.50   1st Qu.:13.85   1st Qu.:16.35  
##  Median :13.40   Median :14.20   Median :16.00   Median :18.70  
##  Mean   :13.49   Mean   :14.35   Mean   :16.00   Mean   :18.80  
##  3rd Qu.:15.40   3rd Qu.:16.60   3rd Qu.:18.55   3rd Qu.:21.80  
##  Max.   :19.50   Max.   :20.50   Max.   :23.10   Max.   :27.40  
##                                                                 
##     Punct.T       Punct.Side         Thick.Tread      Thick.Side   
##  Min.   : 6.00   Length:75          Min.   :1.000   Min.   :0.000  
##  1st Qu.:10.00   Class :character   1st Qu.:2.000   1st Qu.:2.000  
##  Median :11.00   Mode  :character   Median :2.000   Median :6.000  
##  Mean   :11.43                      Mean   :2.236   Mean   :4.847  
##  3rd Qu.:12.00                      3rd Qu.:3.000   3rd Qu.:7.000  
##  Max.   :20.00                      Max.   :4.000   Max.   :9.000  
##                                     NA's   :3       NA's   :3
tires.separated %>% datatable(rownames = F)

One piece I have not been able to correct is that tread thickness (Thick.Tread) should be as a double and separate() saves as an integer. This loses some of the fidelity in the data. I will look at correcting as I learn more R… :)

Plots

Histogram by type of tire:

Relationship between rolling resistance at 80 PSI and Puncture resistance on the tread

We can see a very clear trend between rolling resistance and puncture resistance: the lower the rolling resistance (i.e. the faster the tire), the lower the puncture resistance. The data bears out the logical notion that there is a trade-off between speed and puncture protection

Note that the echo = FALSE parameter was added to the code chunk to prevent printing of the R code that generated the plot.